iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0

當然看過前面的程式後,除了有重複的程式外,也開始會發現有順序問題。像是太陽光最強的地方會蓋過天空色、在漸漸減弱時天空色會再次顯現、甚至後面範例的雲會混合更多的色層。

在這邊為了順利傳遞需要的數值到下一個渲染,就需要 Pass 的概念了。

一般的引擎都會有渲染順序的設定,這邊拿 UE 的 Rendering 教學當作範例。

Begin Play | Rendering | Tutorial

但這些常見的引擎都已經把相關的實作包的非常上層,底層這些 Pass 是怎麼傳遞的得直接開 RenderDoc 才看的到,譬如以下的文章。

How Unreal Renders a Frame

雖然是 2017 的文章了,不過可以看看他是如何分析裡面內容的

不過這專案畢竟是用來熟悉基礎的,先從簡單的設定開始比較好。

因此在這裡,再把前面的程式重新重構一次

render_pass.h

#pragma once

#include <d3d12.h>

class GraphicsEngine;
class RenderContext;
class RootSignature;

//初始化渲染階段時所需的共用資源
struct RenderPassInitContext
{
    GraphicsEngine& graphicsEngine;
    RootSignature& rootSignature;
};

//執行單一畫格的渲染階段時所需的資料
struct RenderFrameContext
{
    RenderContext& renderContext;
    UINT width;
    UINT height;
};

//所有渲染階段的共用介面
class RenderPass
{
  public:
    virtual ~RenderPass() = default;

    //建立此渲染階段所需的固定資源與 pipeline 狀態
    virtual void init(const RenderPassInitContext& context) = 0;

    //將此渲染階段的命令記錄至目前 frame 的 RenderContext
    virtual void execute(const RenderFrameContext& context) = 0;
};

common_pass.h

#pragma once

#include "constant_buffer.h"
#include "math/matrix.h"
#include "pipeline_state.h"
#include "render_pass.h"
#include "shader.h"
#include "math/vector.h"

class CommonPass final : public RenderPass
{
  public:
    void init(const RenderPassInitContext& context) override;
    void execute(const RenderFrameContext& context) override;

  private:
    struct CameraConstants
    {
        Matrix inverseView;
        Matrix inverseProjection;
        Vector3 cameraPosition;
        float padding;
    };

    static_assert(sizeof(CameraConstants) == 144,
                  "CameraConstants must match the HLSL constant-buffer layout.");

    void updateCameraConstants(UINT width, UINT height);

    Shader m_vertexShader;
    Shader m_pixelShader;
    PipelineState m_pipelineState;
    ConstantBuffer m_cameraConstantBuffer;
    CameraConstants m_cameraConstants{};
};
common_pass.cpp

#include "common_pass.h"
#include "cloudscape_camera.h"

#include <stdexcept>

#include <directx/d3dx12_core.h>

#include "graphics_engine.h"
#include "root_signature.h"
#include "render_context.h"

namespace
{
D3D12_GRAPHICS_PIPELINE_STATE_DESC createPipelineStateDescription(
    ID3D12RootSignature* rootSignature, ID3DBlob* vertexShader, ID3DBlob* pixelShader)
{
    if (rootSignature == nullptr)
    {
        throw std::invalid_argument("CommonPass: Root signature is required.");
    }

    if (vertexShader == nullptr)
    {
        throw std::invalid_argument("CommonPass: Vertex shader is required.");
    }

    if (pixelShader == nullptr)
    {
        throw std::invalid_argument("CommonPass: Pixel shader is required.");
    }

     D3D12_GRAPHICS_PIPELINE_STATE_DESC description{};

    description.InputLayout = {nullptr, 0};
    description.pRootSignature = rootSignature;
    description.VS = CD3DX12_SHADER_BYTECODE(vertexShader);
    description.PS = CD3DX12_SHADER_BYTECODE(pixelShader);

    description.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
    description.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;

    description.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);

    description.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
    description.DepthStencilState.DepthEnable = FALSE;
    description.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;
    description.DepthStencilState.StencilEnable = FALSE;

    description.SampleMask = UINT_MAX;
    description.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
    description.NumRenderTargets = 1;
    description.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
    description.DSVFormat = DXGI_FORMAT_D32_FLOAT;
    description.SampleDesc.Count = 1;

    return description;
}
}

void CommonPass::updateCameraConstants(UINT width, UINT height)
{
    const float aspectRatio = static_cast<float>(width) / static_cast<float>(height);
    const Vector3& cameraPosition = CloudscapeCamera::position;
    const Vector3& cameraTarget = CloudscapeCamera::target;
    const Vector3& worldUp = CloudscapeCamera::up;

    Matrix view;
    view.MakeLookAt(cameraPosition, cameraTarget, worldUp);
    Matrix projection;
    projection.MakeProjectionMatrix(Math::PI / 3.0f, aspectRatio, 0.1f, 1000.0f);

    // HLSL 以 mul(vector, matrix) 使用矩陣,因此上傳前需要轉置。
    m_cameraConstants.inverseView.Inverse(view);
    m_cameraConstants.inverseView.Transpose();
    m_cameraConstants.inverseProjection.Inverse(projection);
    m_cameraConstants.inverseProjection.Transpose();
    m_cameraConstants.cameraPosition = cameraPosition;
    m_cameraConstants.padding = 0.0f;
}

void CommonPass::init(const RenderPassInitContext& context)
{
    m_vertexShader.loadVS("assets/shaders/common/common.hlsl", "VSMain");

    m_pixelShader.loadPS("assets/shaders/common/common.hlsl", "PSMain");

    const D3D12_GRAPHICS_PIPELINE_STATE_DESC description = createPipelineStateDescription(
        context.rootSignature.get(), m_vertexShader.getCompiledBlob(), m_pixelShader.getCompiledBlob());

    m_pipelineState.init(description);

    const UINT width = context.graphicsEngine.getFrameBufferWidth();
    const UINT height = context.graphicsEngine.GetFrameBufferHeight();
    if (width == 0 || height == 0)
    {
        throw std::runtime_error("CommonPass: Frame-buffer dimensions must be non-zero during initialization.");
    }

    updateCameraConstants(width, height);
    m_cameraConstantBuffer.init(sizeof(CameraConstants), &m_cameraConstants);
}

void CommonPass::execute(const RenderFrameContext& context)
{
    if (context.width == 0 || context.height == 0)
    {
        return;
    }

    updateCameraConstants(context.width, context.height);
    m_cameraConstantBuffer.copyToVRAM(m_cameraConstants);

    context.renderContext.setPipelineState(m_pipelineState);
    context.renderContext.setGraphicsRootConstantBufferView(
        0, m_cameraConstantBuffer.getGPUVirtualAddress());
    context.renderContext.draw(3);
}
light_pass.h

#pragma once

#include "constant_buffer.h"
#include "common.h"
#include "math/matrix.h"
#include "pipeline_state.h"
#include "render_pass.h"
#include "shader.h"
#include "math/vector.h"

class LightPass final : public RenderPass
{
  public:
    explicit LightPass(const CloudSettings& settings);

    void init(const RenderPassInitContext& context) override;
    void execute(const RenderFrameContext& context) override;
    void setSettings(const CloudSettings& settings);

  private:
    struct SunConstants
    {
        Matrix inverseView;
        Matrix inverseProjection;
        Vector3 sunDirection;
        float sunRadius;
        Vector3 sunColor;
        float sunIntensity;
    };

    static_assert(sizeof(SunConstants) == 160, "SunConstants must match the HLSL constant-buffer layout.");

    void updateCameraConstants(UINT width, UINT height);

    Shader m_vertexShader;
    Shader m_pixelShader;
    PipelineState m_pipelineState;
    ConstantBuffer m_constantBuffer;

    SunConstants m_constants{};
};
light_pass.cpp

#include "light_pass.h"
#include "cloudscape_camera.h"

#include <stdexcept>

#include <directx/d3dx12_core.h>

#include "graphics_engine.h"
#include "root_signature.h"
#include "render_context.h"

namespace
{
D3D12_GRAPHICS_PIPELINE_STATE_DESC createPipelineStateDescription(ID3D12RootSignature* rootSignature,
                                                                  ID3DBlob* vertexShader, ID3DBlob* pixelShader)
{
    if (rootSignature == nullptr)
    {
        throw std::invalid_argument("LightPass: Root signature is required.");
    }

    if (vertexShader == nullptr)
    {
        throw std::invalid_argument("LightPass: Vertex shader is required.");
    }

    if (pixelShader == nullptr)
    {
        throw std::invalid_argument("LightPass: Pixel shader is required.");
    }

    D3D12_GRAPHICS_PIPELINE_STATE_DESC description{};

    description.InputLayout = {nullptr, 0};
    description.pRootSignature = rootSignature;
    description.VS = CD3DX12_SHADER_BYTECODE(vertexShader);
    description.PS = CD3DX12_SHADER_BYTECODE(pixelShader);

    description.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
    description.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;

    description.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);

    D3D12_RENDER_TARGET_BLEND_DESC& renderTarget = description.BlendState.RenderTarget[0];

    renderTarget.BlendEnable = TRUE;
    renderTarget.SrcBlend = D3D12_BLEND_SRC_ALPHA;
    renderTarget.DestBlend = D3D12_BLEND_INV_SRC_ALPHA;
    renderTarget.BlendOp = D3D12_BLEND_OP_ADD;
    renderTarget.SrcBlendAlpha = D3D12_BLEND_ONE;
    renderTarget.DestBlendAlpha = D3D12_BLEND_ZERO;
    renderTarget.BlendOpAlpha = D3D12_BLEND_OP_ADD;
    renderTarget.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;

    description.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
    description.DepthStencilState.DepthEnable = FALSE;
    description.DepthStencilState.DepthWriteMask = D3D12_DEPTH_WRITE_MASK_ZERO;
    description.DepthStencilState.StencilEnable = FALSE;

    description.SampleMask = UINT_MAX;
    description.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
    description.NumRenderTargets = 1;
    description.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
    description.DSVFormat = DXGI_FORMAT_D32_FLOAT;
    description.SampleDesc.Count = 1;

    return description;
}
}

LightPass::LightPass(const CloudSettings& settings)
{
    setSettings(settings);
}

void LightPass::setSettings(const CloudSettings& settings)
{
    m_constants.sunDirection = settings.sunDirection;
    m_constants.sunDirection.Normalize();
    m_constants.sunRadius = 0.055f;
    m_constants.sunColor = settings.sunColor;
    m_constants.sunIntensity = settings.sunIntensity;
}

void LightPass::updateCameraConstants(UINT width, UINT height)
{
    const float aspectRatio = static_cast<float>(width) / static_cast<float>(height);
    const Vector3& cameraPosition = CloudscapeCamera::position;
    const Vector3& cameraTarget = CloudscapeCamera::target;
    const Vector3& worldUp = CloudscapeCamera::up;

    Matrix view;
    view.MakeLookAt(cameraPosition, cameraTarget, worldUp);
    Matrix projection;
    projection.MakeProjectionMatrix(Math::PI / 3.0f, aspectRatio, 0.1f, 1000.0f);

    // HLSL 以 mul(vector, matrix) 使用矩陣,因此上傳前需要轉置。
    m_constants.inverseView.Inverse(view);
    m_constants.inverseView.Transpose();
    m_constants.inverseProjection.Inverse(projection);
    m_constants.inverseProjection.Transpose();
}

void LightPass::init(const RenderPassInitContext& context)
{
    m_vertexShader.loadVS("assets/shaders/sun.hlsl", "VSMain");

    m_pixelShader.loadPS("assets/shaders/sun.hlsl", "PSMain");

    const D3D12_GRAPHICS_PIPELINE_STATE_DESC description = createPipelineStateDescription(
        context.rootSignature.get(), m_vertexShader.getCompiledBlob(), m_pixelShader.getCompiledBlob());

    m_pipelineState.init(description);

    const UINT width = context.graphicsEngine.getFrameBufferWidth();
    const UINT height = context.graphicsEngine.GetFrameBufferHeight();
    if (width == 0 || height == 0)
        throw std::runtime_error("LightPass: Frame-buffer dimensions must be non-zero during initialization.");

    updateCameraConstants(width, height);
    m_constantBuffer.init(sizeof(SunConstants), &m_constants);
}

void LightPass::execute(const RenderFrameContext& context)
{
    if (context.height == 0)
    {
        return;
    }

    updateCameraConstants(context.width, context.height);
    m_constantBuffer.copyToVRAM(m_constants);

    context.renderContext.setPipelineState(m_pipelineState);
    context.renderContext.setGraphicsRootConstantBufferView(0, m_constantBuffer.getGPUVirtualAddress());
    context.renderContext.draw(3);
}

//renderer.h

#pragma once

#include <memory>
#include <vector>

#include "root_signature.h"

class GraphicsEngine;
class LightPass;
class RenderPass;

class Renderer
{
public:
    explicit Renderer(GraphicsEngine& graphicsEngine);
    ~Renderer();

    void init();
    void render();
    void clearRenderItems();

private:
    GraphicsEngine& m_graphicsEngine;

    RootSignature m_rootSignature;

    std::vector<std::unique_ptr<RenderPass>> m_passes;

    LightPass* m_lightPass = nullptr;

    bool m_initialized = false;
};
//renderer.cpp

#include "renderer.h"

#include <stdexcept>

#include "common_pass.h"
#include "graphics_engine.h"
#include "light_pass.h"
#include "render_context.h"
#include "render_pass.h"

// 保存 GraphicsEngine 的參考供 Renderer 後續使用
Renderer::Renderer(GraphicsEngine& graphicsEngine)
    : m_graphicsEngine(graphicsEngine)
{
    // Renderer 建立前必須確認 D3D12 Device 已完成初始化
    if (m_graphicsEngine.getD3DDevice() == nullptr)
    {
        throw std::runtime_error("Graphics engine is not initialized");
    }
}

// 使用編譯器產生的預設解構行為
Renderer::~Renderer() = default;

// 初始化 Renderer 所需的 Root Signature 與各個 Render Pass
void Renderer::init()
{
    // 防止同一個 Renderer 被重複初始化
    if (m_initialized)
    {
        throw std::logic_error(
            "Renderer: Renderer is already initialized.");
    }

    // 初始化 Root Signature 使用的 Static Sampler 設定
    m_rootSignature.init(
        D3D12_FILTER_MIN_MAG_MIP_LINEAR,
        D3D12_TEXTURE_ADDRESS_MODE_WRAP,
        D3D12_TEXTURE_ADDRESS_MODE_WRAP,
        D3D12_TEXTURE_ADDRESS_MODE_WRAP);

    // 將 GraphicsEngine 與 Root Signature 包裝成 Render Pass 初始化環境
    RenderPassInitContext initContext{
        m_graphicsEngine,
        m_rootSignature
    };

    // 建立處理共用繪製工作的 Common Pass
    auto commonPass = std::make_unique<CommonPass>();

    // 初始化 Common Pass 所需的 GPU 資源
    commonPass->init(initContext);

    // 將 Common Pass 所有權移交給 Renderer 統一管理
    m_passes.push_back(std::move(commonPass));

    // 建立 Light Pass
    auto lightPass = std::make_unique<LightPass>();

    // 保存非擁有型指標,供 Renderer 後續使用
    m_lightPass = lightPass.get();

    // 初始化 Light Pass 所需的 GPU 資源
    lightPass->init(initContext);

    // 將 Light Pass 所有權移交給 Renderer 統一管理
    m_passes.push_back(std::move(lightPass));

    // 標記 Renderer 已成功完成初始化
    m_initialized = true;
}

// 依照 Render Pass 順序執行目前影格的所有繪製工作
void Renderer::render()
{
    // 取得目前 GraphicsEngine 使用的 RenderContext
    RenderContext& renderContext =
        m_graphicsEngine.getRenderContext();

    // 將此 Renderer 的 Root Signature 綁定至 Command List
    renderContext.setRootSignature(m_rootSignature);

    // 將圖元拓樸設定為 Triangle List
    renderContext.setPrimitiveTopology(
        D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

    // 建立本影格共用的繪製環境並帶入 Frame Buffer 尺寸
    RenderFrameContext frameContext{
        renderContext,
        m_graphicsEngine.getFrameBufferWidth(),
        m_graphicsEngine.GetFrameBufferHeight()
    };

    // 依照加入順序執行所有 Render Pass
    for (const auto& pass : m_passes)
    {
        // 讓目前 Pass 使用本影格的 RenderContext 執行繪製
        pass->execute(frameContext);
    }
}

main 會像這樣

//main.cpp

#include <cstdlib>
#include <exception>

#include "graphics_engine.h"
#include "my_engine.h"
#include "render_pass/renderer.h"
#include "skyline_debugger.h"
#include "system.h"

int WINAPI wWinMain(HINSTANCE instance, HINSTANCE previous, LPWSTR commandLine, int showCommand)
{
    try
    {
        SkylineDebugger::Initialize("DayX_Pass");

        initWindow(instance, previous, commandLine, showCommand, TEXT("DayX Pass"));

        GraphicsEngine graphicsEngine;
        graphicsEngine.init(g_hWnd, FRAME_BUFFER_W, FRAME_BUFFER_H);

        SkylineDebugger::ConfigureD3D12(graphicsEngine.getD3DDevice());

        Renderer renderer(graphicsEngine);
        renderer.init();

        while (dispatchWindowMessage())
        {
            graphicsEngine.beginRender();
            renderer.render();
            graphicsEngine.endRender();

            SkylineDebugger::LogD3D12Messages(graphicsEngine.getD3DDevice());
        }

        SkylineDebugger::Shutdown();
        return EXIT_SUCCESS;
    }
    catch (const std::exception& error)
    {
        SkylineDebugger::ShowFatalError("DayX_Pass initialization failed", error);
        SkylineDebugger::Shutdown();
        return EXIT_FAILURE;
    }
    catch (...)
    {
        SkylineDebugger::ShowFatalError("DayX_Pass initialization failed",
                                        "An unknown fatal error occurred.");
        SkylineDebugger::Shutdown();
        return EXIT_FAILURE;
    }
}

參考資料

DirectX 12の魔導書 3Dレンダリングの基礎からMMDモデルを踊らせるまで

Begin Play | Rendering | Tutorial

How Unreal Renders a Frame


上一篇
Day 15:Model View Projection
下一篇
Day 17:體積雲概覽
系列文
因為 AI 看不懂老舊程式,只好乖乖從零開始學 DirectX 12 與 HLSL23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言